import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ServerNotActiveException;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;
import java.util.Vector;

public class Server implements ServerAPI 
{
	private GameStatus gameStatus;
	private boolean joinGameFirstRequestDone;
	private boolean takeRequests;
	private boolean firstAlreadyDone = false;
	private Client client;
	private String registryBindString;
	private int serverType;
	private int port;
	Vector<ServerInformation> serverInfo;
	private ServerAPI backUpStub;
	private ServerAPI mainStub;
	private boolean backUpAssigned;
	ServerInformation nextServer;

	public Server(Client client, int id) {
		this.client = client;
		takeRequests = true;
		this.registryBindString = "ServerAPI" + id;
		serverType = 2; // Normal server
		gameStatus = null;
	}
	
    public Server(int gridSize, int noOfTreasures, int port) 
    {
		gameStatus = new GameStatus(gridSize, noOfTreasures);
		joinGameFirstRequestDone = false;
		takeRequests = true;
		registryBindString = "ServerAPI0";
		serverType = 0; // Main server
		this.port = port;
		serverInfo = new Vector<ServerInformation>();
		backUpAssigned = false;
    }
    
	public Client getClient() {
		return client;
	}

	public void setClient(Client client) {
		this.client = client;
	}

    public boolean getJoinGameFirstRequestDone()
    {
    	return getsetJoinGameFirstRequestDone(true, false);
    }
    
    public void setJoinGameFirstRequestDone(boolean set)
    {
    	getsetJoinGameFirstRequestDone(set, true);
    }
    
    // This is to ensure parallel execution
	private synchronized boolean getsetJoinGameFirstRequestDone(
			boolean joinGameFirstRequestDone, boolean isSet) {
		if (isSet)
		{
			if (firstAlreadyDone)
				return false;
			else 
			{
				this.joinGameFirstRequestDone = joinGameFirstRequestDone;
				firstAlreadyDone = true;
			}
		}
		else
		{
			return this.joinGameFirstRequestDone;
		}
		return false;
	}
	
	public boolean getTakeRequests()
	{
		return getsetTakeRequests(true, false);
	}
	
	public void setTakeRequests(boolean set)
	{
		getsetTakeRequests(set, true);
	}
	
	public void startTwentySecondTimer()
	{
		while(!getsetJoinGameFirstRequestDone(false, false))
		{
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				System.out.println("Start Twenty second timer thread problem");
			}
		}
		System.out.println("Server: Got the first request to join. Starting 20 second counter");
		try {
			Thread.sleep(20000);
		} catch (InterruptedException e) {
			System.out.println("Start Twenty second timer thread problem in 20 second wait");
		}
		
		setTakeRequests(false);

		System.out.println("Server: The 20 seconds are up, no player can join now");
	}
	
	// Same reason as join game first request
	private synchronized boolean getsetTakeRequests(boolean takeRequests, boolean isSet) {
		if (isSet)
		{
			this.takeRequests = takeRequests;
		}
		else
		{
			return this.takeRequests;
		}
		return false;
	}

    
	public Pair<Boolean, GameStatus> move(int id, int direction)
	{
		Boolean moveSuccess = gameStatus.move(id, direction, backUpStub, serverType);
		Pair<Boolean, GameStatus> pair = new Pair<Boolean, GameStatus>(moveSuccess, gameStatus);
		return pair;
	}
	
	public Pair<Integer, Integer> updateBackUpmove(int id, int direction)
	{
		return gameStatus.moveBackUp(id, direction);
	}
	
	public Pair<Boolean, Integer> joinGame(Boolean mainServer)
			throws RemoteException {
		Pair<Boolean, Integer> pair = null;
		if (!mainServer && !getTakeRequests()) {
			System.out
					.println("Server: Player tried joining after 20 second period. Request Denied");
			pair = new Pair<Boolean, Integer>(false, -1);
		} else {
			if (!mainServer)
				setJoinGameFirstRequestDone(true);

			int id = gameStatus.addPlayer();
			pair = new Pair<Boolean, Integer>(true, id);
			if (mainServer)
				System.out.println("Server: Main Server Ready");
			System.out.println("Server: Player " + id + " joined");

		}
		return pair;
	}

	public void startAccepting() {
		new Thread(new Runnable() {
			public void run() {
				startTwentySecondTimer();
			}
		}).start();

	}

	// Starting the server
	public static void start(Server obj) {
		if (obj.serverType == 0) {
			obj.startAccepting();
		}
		ServerAPI stub = null;
		Registry registry = null;

		try {
			stub = (ServerAPI) UnicastRemoteObject.exportObject(obj, 0);
			if (obj.port == -1) {
				registry = LocateRegistry.getRegistry();
			} else {
				registry = LocateRegistry.getRegistry(obj.port);
			}
			registry.bind(obj.registryBindString, stub);
		} catch (Exception e) {
			try {
				registry.unbind(obj.registryBindString);
				registry.bind(obj.registryBindString, stub);
			} catch (Exception ee) {
				System.err.println("Exception in static start");
			}
		}
	}

	// This will set the back up server
	private synchronized void setBackUpServer(String host, int port, int id) {
		if (serverInfo.isEmpty()) {
			System.out.println("Server: The backup server has an id " + id);
			try {

				Registry registry = null;

				if (port == -1)
					registry = LocateRegistry.getRegistry(host);
				else
					registry = LocateRegistry.getRegistry(host, port);
				backUpStub = (ServerAPI) registry.lookup("ServerAPI" + id);
				backUpStub.assignServerAsBackUp(this.port, this.registryBindString);

				new Thread(new Runnable() {
					public void run() {
						checkBackUpServer();
					}
				}).start();

				backUpAssigned = true;
				serverInfo.add(new ServerInformation(host, port, id, false));
			} catch (Exception e) {
				System.err.println("Exception in setting back up server\n" + e.toString());
			}

		}
		serverInfo.add(new ServerInformation(host, port, id, true));
	}

	public void assignServerAsBackUp(int port, String registryBindString) {
		serverType = 1;
		try {
			String host = java.rmi.server.RemoteServer.getClientHost();

			Registry registry = null;

			if (port == -1)
				registry = LocateRegistry.getRegistry(host);
			else
				registry = LocateRegistry.getRegistry(host, port);
			mainStub = (ServerAPI) registry.lookup(registryBindString);
			System.out.println("Back Up Server: Back server is ready");
			
			new Thread(new Runnable() {
				public void run() {
					checkMainServer();
				}
			}).start();

		} catch (Exception e) {
			System.err.println("Server exception: while assigning back up server");
		}

	}

	public StartGameStatus waitToPlay(int id, int myPort)
			throws RemoteException {
		String host;
		try {
			host = java.rmi.server.RemoteServer.getClientHost();
		} catch (ServerNotActiveException e1) {
			System.out.println("Server: Can't get IP address of client");
			host = null;
		}
		if (id != 0) {
			setBackUpServer(host, port, id);
		}
		while (getTakeRequests()) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				System.out.println("Server: Error in waiting in joinGame");
			}
		}
		StartGameStatus sgs = new StartGameStatus(gameStatus, serverInfo.get(0));
		if (gameStatus.getPositionOfPlayers().size() == (id+1))
		{
			while (!backUpAssigned);
			if (gameStatus.getPositionOfPlayers().size() == (id+1))
			{
				backUpStub.updateBackUpBegin(gameStatus, serverInfo);
			}
		}
		return sgs;
	}
	
	public void updateBackUpBegin(GameStatus gameStatus, Vector<ServerInformation> serverInfo)
	{
		setGameStatus(gameStatus);
		this.serverInfo = serverInfo;
	}
	
	public synchronized void setGameStatus(GameStatus gameStatus)
	{
		if ((this.gameStatus == null) || (gameStatus.getPositionOfPlayers().size() > this.gameStatus.getPositionOfPlayers().size() ))
		{
			this.gameStatus = gameStatus;
		}
		System.out.println("Back up Server: Game Status updated");
		gameStatus.toString();
	}
	
	private void checkMainServer()
	{
		while(true)
		{
			if (serverType!=0)
			{
				try {
					mainStub.isBackUpServerAlive();
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						System.out.println("Server: Error in waiting checkMainServer");
					}

				} catch (RemoteException e) {
					System.out.println("Back up Server: Main server has crashed");
					System.out.println("Changing myself to main server and getting new Back up Server");
					findNextServer();
					System.out.println("The server that will become back Up Server has id "+nextServer.getId());
					try 
					{
						Registry registry = null;
						
						if (nextServer.getPort() == -1)
						    registry = LocateRegistry.getRegistry(nextServer.getHost());
						else
							registry = LocateRegistry.getRegistry(nextServer.getHost(), nextServer.getPort());
					    ServerAPI stub = (ServerAPI) registry.lookup("ServerAPI"+nextServer.getId());
					    stub.assignServerAsBackUp(this.port, this.registryBindString);
					    stub.updateBackUpBegin(this.gameStatus, this.serverInfo);
					    backUpStub = stub;
					    System.out.println("The new back up server is at "+nextServer.getHost()+" at port "
					    		+nextServer.getPort()+" with id "+nextServer.getId());
					    nextServer = null;
					    serverType = 0;
					    System.out.println("Server: Telling everone about the new main server");
					    announceChangeInServer(0);
					    System.out.println("Server: Telling everone about the new back up server");
					    announceChangeInServer(1);
					    break;
					}
					catch (Exception ee)
					{
					    System.err.println("Server: Error in setting up new server");
					}

				}
			}
		}
		checkBackUpServer();
	}

	private void checkBackUpServer()
	{
		while(true)
		{
			if (serverType==0)
			{
				try 
				{
					backUpStub.isBackUpServerAlive();
					try 
					{
						Thread.sleep(1000);
					} 
					catch (InterruptedException e) 
					{
						System.out.println("Error in waiting in checkBackUpServer");
					}

				} 
				catch (RemoteException e) 
				{
					System.out.println("Server: Back up server has crashed");
					System.out.println("Server: Getting new Back up Server");
					findNextServer();
					try 
					{
						Registry registry = null;
						
						if (nextServer.getPort() == -1)
						    registry = LocateRegistry.getRegistry(nextServer.getHost());
						else
							registry = LocateRegistry.getRegistry(nextServer.getHost(), nextServer.getPort());
					    ServerAPI stub = (ServerAPI) registry.lookup("ServerAPI"+nextServer.getId());
					    stub.assignServerAsBackUp(this.port, this.registryBindString);
					    stub.updateBackUppSynchronised(this.gameStatus, this.serverInfo, true);
					    backUpStub = stub;
					    System.out.println("Server: The new back up server is at "+nextServer.getHost()+" at port "
					    		+nextServer.getPort()+" with id "+nextServer.getId());
					    nextServer = null;
					    announceChangeInServer(1);
					}
					catch (Exception ee)
					{
					    System.err.println("Server: Error in setting up new Back up server");
					}
				}
			}
		}
	}
	
	public synchronized void updateBackUppSynchronised(GameStatus gs, Vector<ServerInformation> si, boolean hasCrashed) throws RemoteException
	{
		if (hasCrashed)
		{
			updateBackUpBegin(gs, si);
		}
		else
		{
			this.backUpUpdate(gs);
		}
	}
	
	public void announceChangeInServer(int serverType)
	{
		ServerAPI temp = null;
		if (serverType == 1)
			temp = backUpStub;
		else temp = this;
		
		final int serverT = serverType;
		
		final ServerAPI stub = temp;
		for (int i=0;i<serverInfo.size();i++)
		{
			final ServerInformation si = serverInfo.get(i);
			new Thread(new Runnable() {
				public void run() {
					updateWithNewServer(si, serverT, stub);
				}
			}).start();
		}
	}
	
	public void updateWithNewServer(ServerInformation si, int serverType, ServerAPI newStub)
	{
		try 
		{
			Registry registry = null;
			
			if (port == -1)
			    registry = LocateRegistry.getRegistry(si.getHost());
			else
				registry = LocateRegistry.getRegistry(si.getHost(), si.getPort());
		    ServerAPI stub = (ServerAPI) registry.lookup("ServerAPI"+si.getId());
		    
		    stub.setNewServer(newStub, serverType);
		} catch (Exception e) {
		}		
	}
	
	public void connectToServer(ServerInformation si)
	{
		try {
			Registry registry = null;
			
			if (si.getPort() == -1)
			    registry = LocateRegistry.getRegistry(si.getHost());
			else
				registry = LocateRegistry.getRegistry(si.getHost(), si.getPort());
		    ServerAPI stub = (ServerAPI) registry.lookup("ServerAPI"+si.getId());
		    stub.isServerAlive();
		    setNextServerIfNotAlreadyDone(si);
		} catch (Exception e) {
		    System.err.println("Server with host "+si.getHost()+" with port "+si.getId()+
		    		" with id "+si.getId()+" is not alive");
		}		
	}
	
	private synchronized void setNextServerIfNotAlreadyDone(ServerInformation si)
	{
		if (nextServer == null)
		{
			System.out.println("First to respond (and this back up server) is id "+si.getId());
			nextServer = si;
		}
	}
	
	private void findNextServer()
	{
		for (int i=0;i<serverInfo.size();i++)
		{
			final ServerInformation temp = serverInfo.get(i);
			String tempS = this.registryBindString.substring(9);
			int tId = Integer.parseInt(tempS);
			if (temp.isTryToConnect() && temp.getId()!= tId)
			{
				System.out.println("Trying to connect to server with id "+temp.getId());
				new Thread(new Runnable() {
					public void run() {
						connectToServer(temp);
					}
				}).start();

			}
		}
		while (nextServer == null)
		{
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				System.out.println("Error in findNextServer");
			}			
		}
	}
	
	
	
	public boolean isMainServerAlive() throws RemoteException 
	{
		return false;
	}

	public boolean isBackUpServerAlive() throws RemoteException 
	{
		return false;
	}

	
	private void backUpUpdate(GameStatus gameStatus) throws RemoteException 
	{
		this.gameStatus = gameStatus;
	}

	public void isServerAlive() throws RemoteException 
	{
		
		return;
	}

	public void setNewServer(ServerAPI stub, int serverType) throws RemoteException 
	{
		if (serverType == 1)
			client.setBackUpStub(stub);
		else
			client.setStub(stub);
	}

	public void printIdentity(int serverType, int id) throws RemoteException 
	{
		if (serverType == 0)
			System.out.println("Server: I am now the main server for player with id "+id);
		else System.out.println("Server: I am now the back up server for player with id "+id);
	}
}
